StupidBeauty
Read times:1555Posted at:Mon Mar 4 00:17:32 2013
- no title specified

Wt3.2.3文档翻译:Wt::Dbo教程,Wt::Dbo Tutorial

Koen Deforche <koen@emweb.be> 3.1.10,2011年7月14

1. 介绍

Wt::Dbo 是一个C++对象关系映射(ORM (Object-Relational-Mapping))库。

这个库是与 Wt 一同发布的,可用来构建数据库驱动的网页程序。 但也可完全独立使用。

这个库在数据表之上提供了一个基于类的视图,其中维护了一个由数据库对象组成的对象层次关系, 这个关系会在插入、更新和删除数据库记录时自动与数据库保持同步。C++类会被映射为数据库表,类的成员会被映射成 表中的列 ,指针及指针的集合被映射为数据 库中的关系。 被映射的类的一个对象叫做 数据库对象 (dbo) 。查询结果 可以以下形式定义:数据 库对象 、元数据或数据 元组。

我们使用了一种现代 C++方式 来处理映射问题。 我们不是使用XML 来描述C++类 和成员 与表和列之间 的映射规则 ,也不是使用晦涩难懂的宏, 而是使用纯 净的C++代码 来定义映射规则。

在这 教程中, 我们会 一路向西开发 出一个勃客示例程序 这个示例程序将会与随整个 库一起 发布的那个 勃客示例程序类似。

提示

Wt examples/feature/dbo/ 目录中已经包含了此教程 中所用示例 的完整代码 ,可直接编译后运行。

2. 映射单个类

我们从简单的例子开始,使用 Wt::Dbo 来将单个的类 User 映射到 user 这个数据库表。

警告

在这个教程以及示例中, 我们会 Wt::Dbo 命名空间 起个别名 dbo 。而在我们的解说当中,会直接引用那个完整命名空间 中的类型 和方法。

在编译以下示例的时候,妳需要链接至 wtdbo wtdbosqlite3 库。

映射单个(tutorial1.C)

#include <Wt/Dbo/Dbo>

#include <string>

namespace dbo = Wt :: Dbo ;

class User {

public :

enum Role {

Visitor = 0 ,

Admin = 1 ,

Alien = 42

} ;

std :: string name ;

std :: string password ;

Role role ;

int karma ;

template < class Action >

void persist ( Action & a )

{

dbo :: field ( a , name , "name" );

dbo :: field ( a , password , "password" );

dbo :: field ( a , role , "role" );

dbo :: field ( a , karma , "karma" );

}

} ;

这个示例演示了,如何为一个C++类定义持久 化支持。 这里定义了一个模板成员函数 persist() 它就是这个类的持久化定义。对于 这个类中的每个成员, 都会调用 Wt::Dbo::field() 来将它映射为数据 中的一个列。

妳可能已经注意 到了, 这个库已经支持标准 的C++ 类型 Wt::Dbo::sql_value_traits< T > 文档中可找到一份完整的 支持类型列表 了,例如 int std::string enum 类型。如果 想为其它类型提供支持的话,只需要特化 Wt::Dbo::sql_value_traits< T > 就可以了。另外 它还支持Wt 内置的一些类型 ,例如WDate WDateTime WTime WString,只需要包含<Wt/Dbo/WtSqlTraits>。

这个库定义了一系列的动作(actions) ,它们 会在类的 persist() 方法中依次 针对 类的所有成员应用 到一个数据库对象上 这些动作对象会读取 、更新、或插入数据 库对象 会创建数据 库模式, 会传递事务结果

注意

简单起见,我们 在这个示例中使用了公有成员。实际 上妳完全可以 将类的状态封装成私有成员 ,并且提供访问函数。 妳甚至还可以根据访问函数 来定义持久 化函数 ,以便 在读取和写入过程中使用不同的动作对象。

3. 第一个会话

既然我们已经 User 类定义好了映射规则, 那我们就可以开始干活 了: 搞出一个数据库会话, (必要的时候)创建数据库模式, 再向数据库里添加一个用户。

让我们 将代码过一遍。

(tutorial1.C继续)

void run ()

{

/*

* 创建 一个会话, 一般情况下 ,只需在程序启动 做一次

*/

dbo :: backend :: Sqlite3 sqlite3 ( "blog.db" );

dbo :: Session session ;

session . setConnection ( sqlite3 );

...

Session 对象是一个长久存在的对象,提供 了对我们的数据库对象进行访问的功能。 一般情况下, 在由同一个用户引发的整个程序会话时间里,妳只需要创建并使用 同一个Session 对象。 Wt::Dbo 中的所有类都不是线程安全的(连接 池是例外 ), 而数据库会话对象是不 会在 程序会话 之间共享的。

我们并不是因为偷懒而不提供线程安全性的。 它是符合数据库 的事务完整性承诺的: 妳不希望妳在某个会话中 还没提交的变更直接 就出现在另一个会话中了(提交 读( Read-Committed )事务隔离级别 )。当然 ,未来可能 有必要实现一个 写时复制(copy-on-write)策略, 这样就可以 在会话之间共享 大庹的数据库对象了。

要向会话提供一个连接对象, 它使用这个连接对象来与数据库通信。会话只在事务 的持续过程中 才会真正使用连接对象 ,因此 不需要给它提供一个独立的连接。 如果 要实现多个并行会话 的话,那么使用连接 池会有更好的效果 ,同时,会话对象也可以使用 连接 的引用来初始化。

Wt::Dbo使用一个抽象层来访问数据库 ,目前支持 Postgres Sqlite3 两种后端

(tutorial1.C继续)

...

session . mapClass < User >( "user" );

/*

* 尝试创建模式(如果已经 有了,则会失败 )

*/

session . createTables ();

...

然后 我们使用 mapClass() 将每个数据 库类注册到会话中, 以表明要将数据 表映射到哪个类上。

,在开发过程中,以及在初始部署的过程中, Wt::Dbo 来创建或删除数据库模式是狠方便的。

这将会生成以下结构化查询语言(SQL)语句:

begin transaction

create table "user" (

"id" integer primary key autoincrement ,

"version" integer not null ,

"name" text not null ,

"password" text not null ,

"role" integer not null ,

"karma" integer not null

)

commit transaction

妳看到了吧, 除了 那4个映射 到C++字段的列之外,默认情况 下, Wt::Dbo 还添加了另两个列: id version id是一个代理(surrogate)主键, version被用作一个基于版本的乐观(optimistic)锁。自从Wt 3.1.4 以来,在Wt::Dbo 中, 妳可以禁用version 字段 ,并且 可使用任何类型 的自然 主键,而不是使用代理 主键,参考 对映射进行自定义

好了,我们可以向数据库中添加一个用户了。所有 的数据库操作都在一个事务中发生。

(tutorial1.C继续)

...

/*

* 每一个小单元的工作都是在一个事务之内发生的。

*/

dbo :: Transaction transaction ( session );

User * user = new User ();

user -> name = "Joe" ;

user -> password = "Secret" ;

user -> role = User :: Visitor ;

user -> karma = 13 ;

dbo :: ptr<User> userPtr = session . add ( user );

transaction . commit ();

}

调用Session::add() 就会向数据库中添加一个对象。 这个调用会返回一个 ptr< Dbo > ,它指向一个类型为 Dbo 的数据库对象。 这是一个共享指针, 它同时会跟踪 被引用的对象的持久化状态。 在每个会话当中,单个数据 库对象最多只会被载入一次:会话 会跟踪那些已载入的数据库对象 ,并且每当 有数据 查询命中 了已有的数据库对象时直接返回该对象。 当某个数据 库对象的最后一个指针离开 其作用域时 该数据库对象的 临时 (位于内存 )副本 也会被删除(除非 它已被修改了, 在那种情况下, 会在将变更提交到数据库之后才删除副本 )。

会话还会跟踪那些 已被修改因而需要刷新(使用结构 化查询语言语句 )到数据库中去的对象 。刷新 会在以下情况下自动发生:提交事务 ;或者 ,需要维护临时副本 与数据库内容之间 的一致性之时(例如,进行 一个查询之前 )。

这将生成以下结构化查询语言语句:

begin transaction

insert into "user" ( "version" , "name" , "password" , "role" , "karma" ) values (?, ?, ?, ?, ?)

commit transaction

所有的结构化查询语言语句都会被预处理一次(对于每个连接 来说 ),然后被复用 这样的好处就是,避免结构 化查询语句注入问题,并且 可获得潜在的性能提升。

4. 查询对象

有两种查询数据库的方式。 可使用 Session::find<Dbo>( condition ) 来查询出单个 Dbo 类实例的数据库对象:

(tutorial1.C继续)

dbo :: ptr<User> joe = session . find < User >(). where ( "name = ?" ). bind ( "Joe" );

std :: cerr << "Joe has karma: " << joe -> karma << std :: endl ;

所有的查询都是预处理过的语句, 进行了 基于位置的参数绑定。 Session::find<T>()方法返回一个 Query< ptr<T> > 对象。Query对象可通过添加结构 化查询语言 where order by group by 定义的方式来对查询进行微调(refine), 还可以使用 Query::bind() 来绑定参数。 在这种情况下, 这个查询应当预期得到单个结果 ,并且 被直接转换成 一个数据库对象指针。

注意

从Wt 3.1.3 开始, Query 类具有了第二个参数 BindStrategy 它有两个可能的取值,分别对应 于两种不同的查询实现方式

默认策略是动态绑定( DynamicBinding ), 这使得此查询成为一个长期存在的对象,与会话关联 到一起 ,并且 可以 多次运行。 同时,妳可以通过 修改排序规则、限制条件或偏移 修改查询条件

另一种策略是直接绑定( DirectBinding ), 它会将绑定的参数直接传递到 底层的一个预处理的语句中。 这也是Query 对象 在改版之前的标准行为。 这样的一个查询只能被运行一次,但是好处 就是(C++)开销较小 ,因为那些参数 的值都是直接传递给后端的,而不是储存 于查询对象之中的。

格式化之后发送给数据库的查询是这样的:

select id , version , "name" , "password" , "role" , "karma"

from "user"

where ( name = ?)

更通用的查询方式是使用 Session::query<Result>( sql ) 它就不仅仅支持数据 库对象作为查询结果了。 以上的查询与下面的代码等价:

(tutorial1.C继续)

dbo :: ptr<User> joe2 = session . query < dbo :: ptr < User > >( "select u from user u" ). where ( "name = ?" ). bind ( "Joe" );

这将生成类似的结构化查询语言语句:

select u . id , u . version , u . "name" , u . "password" , u . "role" , u . "karma"

from user u

where ( name = ?)

传递给此方法的 sql 语句,可以是 其返回值 Result 类型兼容 的任何结构 化查询语言 语句。 这里的结构化查询语言语句中的 select 部分可以重写 就样上面的示例中那样 ), 以返回 查询到的数据库对象的某些单个字段。

这里演示一下 Session::query<Result>() 可用来返回其它类型的结果。研究 一下 以下的这个查询,它会返回一个 int 类型的结果。

(tutorial1.C继续)

int count = session . query < int >( "select count(1) from user" ). where ( "name = ?" ). bind ( "Joe" );

以上的这些查询都预期得到单一 的结果,但是查询 也可能返回多个结果的。所以, Session::query< Result >()可能会返回 一个 dbo::collection< Result > (对于多个结果 的情况 ), 而在以上的示例中 ,为了简单起见, 它们被强制转换成了单个 Result 。类似 地, Session::find< Dbo >()可能会返回一个 collection< ptr< Dbo > > 或一个单个的 ptr<_Dbo > 。如果 要求得到一个单个的结果, 而查询却找到 了多个结果,则 会抛出 NoUniqueResultException 异常。

collection< T > 是一个与标准模板 库(STL)兼容的集合 (collection), 它拥有 迭代器,实现 InputIterator 需求 。所以, 妳只能对一个集合中的结果遍历一次。 在那些结果被遍历过了之后, 这个 collection 对象便不可再使用 (但是对应 Query 对象可以被复用,除非使用 了直接绑定 DirectBinding 策略 )。

以下代码演示的是如何 对一个查询所返回的多个结果进行遍历:

(tutorial1.C继续)

typedef dbo :: collection< dbo::ptr<User> > Users ;

Users users = session . find < User >();

std :: cerr << "We have " << users . size () << " users:" << std :: endl ;

for ( Users :: const_iterator i = users . begin (); i != users . end (); ++ i )

std :: cerr << " user " << (* i )-> name

<< " with karma of " << (* i )-> karma << std :: endl ;

这段代码会进行两次数据库查询 一次是对应于 collection::size() 的调用, 另一次对应于结果 的遍历:

select count ( 1 ) from "user"

select id , version , "name" , "password" , "role" , "karma" from "user"

警告

一个查询对象会使用预先准备好的语句来执行,如果之前 没有为该查询准备过语句的话,会准备 一个新的语句。由于预先准备 的语句通常都不是可重入的, 同时呢,查询对象 在发现存在已有 的语句的时候会使用已有 的语句 ,所以, 妳需要注意, 不要 让两个对应于同一个语句的结果集合 同时处于活跃状态。所以, 在遍历某个查询的结果的时候, 妳不能 次使用该查询。所以 呢,也许 有必要在遍历之前将结果复制 到一个标准容器中(例如 std::vector )。 从Wt 3.1.3 开始, 会检测到并行使用 的情况,并且 会抛出异常,其内容为:

A collection for '...' is already in use. Reentrant statement use is not yet implemented.

我们计划在以后的版本中移除 这个限制 在必要的情况下 对预先准备的语句进行复制。

5. 更新对象

大部分其它的智能指针不同, ptr< Dbo > 在默认情况下是只读的: 它返回一个 const Dbo * 要想 修改一个数据库对象的话,妳需要调用 ptr::modify() 方法, 该方法会返回一个非常量对象。 这会将该对象标记为脏的,并且 在稍后会将 对它的变更 同步到数据库中。

(tutorial1.C继续)

dbo :: ptr<User> joe = session . find < User >(). where ( "name = ?" ). bind ( "Joe" );

joe . modify ()-> karma ++;

joe . modify ()-> password = "public" ;

数据 库同步并不是立即发生的,相反 ,它们 会被延迟,直到: 式通过 ptr< Dbo >::flush() Session::flush() 要求 同步; 某个查询被执行,而该查询 的结果可能会被已进行的修改影响;事务 被提交。

之前的代码会生成以下结构化查询语言语句:

select id , version , "name" , "password" , "role" , "karma"

from "user"

where ( name = ?)

update "user" set "version" = ?, "name" = ?, "password" = ?, "role" = ?, "karma" = ?

where "id" = ? and "version" = ?

我们已经了解过如何使用 Session::add(ptr< Dbo >) 了, 我们使用它向数据库里添加了一个新对象。 与之相对的操作是 ptr< Dbo >::remove() 它会删除数据库中对应的对象。

(tutorial1.C继续)

dbo :: ptr<User> joe = session . find < User >(). where ( "name = ?" ). bind ( "Joe" );

joe . remove ();

在删除了一个对象之后,内存 中的副本对象仍然可以使用,甚至可以重新添加到数据库中去。

注意

就像 modify() 一样, add() remove()操作也是延迟与数据库同步的,所以,下面 这段代码对于数据库不起任何作用:

(tutorial1.C继续)

dbo :: ptr<User> silly = session . add ( new User ());

silly . modify ()-> name = "Silly" ;

silly . remove ();

6. 映射关系

6.1. 多对一( Many-to-One )关系

让我们来向勃客示例中添加文章 吧,并且定义文章 与用户之间多对一的关系。 在以下的代码中, 我们重点关注那些用来定义关系 的语句。

多对一关系(tutorial2.C)

#include <Wt/Dbo/Dbo>

#include <string>

namespace dbo = Wt :: Dbo ;

class User ;

class Post {

public :

...

dbo :: ptr<User> user ;

template < class Action >

void persist ( Action & a )

{

...

dbo :: belongsTo ( a , user , "user" );

}

} ;

class User {

public :

...

dbo :: collection< dbo::ptr<Post> > posts ;

template < class Action >

void persist ( Action & a )

{

...

dbo :: hasMany ( a , posts , dbo :: ManyToOne , "user" );

}

} ;

的一方, 我们加上对一个用户的引用,并且 persist() 方法 中调用 belongsTo() 这使得我们可以引用作为此篇文章的主人的那个用户。 最后一个参数,对应的就是定义了此关系的数据 库列的名字。

的一方, 我们加上 一个文章集合,并且 persist() 方法中调用 hasMany() 。此处 的连接(join)字段必须与前面所述(reciproce)的belongsTo()方法中的字段名相同。

如果我们也使用 Session::mapClass() 来将文章(Post)类添加到我们的会话中,并且创建数据库模式 的话,则会生成以下结构化查询语言语句:

create table "user" (

...

-- user 表不会受到此关系的影响

);

create table "post" (

...

"user_id" bigint ,

constraint "fk_post_user" foreign key ( "user_id" ) references "user" ( "id" )

)

注意 user_id 字段对应 于连接 名“user”。

的一方, 妳可以通过那个 ptr 对象来读写此文章所归属的用户。

的一方的那个集合,使得 我们可 以获取到所有关联的元素、插入 (insert())和删除(remove())元素。 其效果就跟在 的一方设置 ptr 是一样的。

示例:

(tutorial2.C继续)

dbo :: ptr<Post> post = session . add ( new Post ());

post . modify ()-> user = joe ; // 或者 joe.modify()->posts.insert(post);

// 会输出 'Joe has 1 post(s).'

std :: cerr << "Joe has " << joe -> posts . size () << " post(s)." << std :: endl ;

如妳所见, 一旦 joe 设置成 新的文章的用户 user ), 该文章也自动被反射(reflected)到 joe 的文章( posts )集合中去了, 反过来也一样。

警告

集合会使用一个预先准备的语句来执行。集合之间 会尝试着共享单个 的预先准备的语句 ,但是预先准备 的语句通常不是可重入的。由此导致 妳需要格外小心, 不要让对应于同一个语句的两个集合同时处于操作 (busy) 状态。因此 在遍历一个集合的时候, 妳需要注意不要再次遍历这同一个集合(无论是同一个对象或是另一个)。所以,也许 有必要在遍历之前 将结果复制到一个标准容器(例如 std::vector )中去。

我们计划在以后的版本中移除 这个限制 在必要的情况下 对预先准备的语句进行复制。

6.2. 多对多( Many-to-Many )关系

为了演示 多对多 关系, 我们向勃客示例中加入标签(tags),并且 在文章和标签之间定义一个 多对多 关系。 在以下的代码中, 我们依然重点关注那些用来定义关系 的语句。

多对多关系(tutorial2.C)

#include <Wt/Dbo/Dbo>

#include <string>

namespace dbo = Wt :: Dbo ;

class Tag ;

class Post {

public :

...

dbo :: collection< dbo::ptr<Tag> > tags ;

template < class Action >

void persist ( Action & a )

{

...

dbo :: hasMany ( a , tags , dbo :: ManyToMany , "post_tags" );

}

} ;

class Tag {

public :

...

dbo :: collection< dbo::ptr<Post> > posts ;

template < class Action >

void persist ( Action & a )

{

...

dbo :: hasMany ( a , posts , dbo :: ManyToMany , "post_tags" );

}

} ;

聪明的妳想必早已猜到了, 在两个类中,关系是几乎同样地反映出来的: 它们都拥有 一个集合 collection ),包含 了相关的类的数据库对象, 而在 persist() 方法中,我们调用 hasMany() 在这种情况下,所使用的连接字段 对应于用来表示 此关系 的连接表的名字。

使用Session::mapClass() 来将文章 (Post)(☯: 这里可能是Tag,标签类 )类添加到我们的会话中 。现在 在创建数据库模式的时候,会生成以下的结构化查询语言语句:

create table "post" (

...

-- post 表不受此关系的影响

)

create table "tag" (

...

-- tag 表不受此关系的影响

)

create table "post_tags" (

"post_id" bigint not null ,

"tag_id" bigint not null ,

primary key ( "post_id" , "tag_id" ),

constraint "fk_post_tags_key1" foreign key ( "post_id" ) references "post" ( "id" ),

constraint "fk_post_tags_key2" foreign key ( "tag_id" ) references "tag" ( "id" )

)

create index "post_tags_post" on "post_tags" ( "post_id" )

create index "post_tags_tag" on "post_tags" ( "tag_id" )

在这个 多对多 关系中, 每一方的集合都允许我们获取所有 的关联元素。然而 ,与 一个 多对一 关系 中的集合不同的是, 我们现在可以对集合插入 insert() )和删除( erase() )其中 条目。 要定义一篇文章和一个标签之间的关系的话,妳需要 做以下两件事之一: 将文章添加到 该标签的 posts 集合中;或者 将该标签添加到该文章的 tags 集合中。 妳不能两件事都做 !变更 会自动反射到对方对象 的集合中。 同样地, 要想断开一篇文章与一个标签之间的关系的话, 妳应当 从文章的 tags 集合中删除该标签,或者从该标签的 posts 集合中删除该文章 ,但不要在两个集合中都删除对方。

示例:

(tutorial2.C继续)

dbo :: ptr<Post> post = ...

dbo :: ptr<Tag> cooking = session . add ( new Tag ());

cooking . modify ()-> name = "Cooking" ;

post . modify ()-> tags . insert ( cooking );

// 会输出 '1 post(s) tagged with Cooking.'

std :: cerr << cooking -> posts . size () << " post(s) tagged with Cooking." << std :: endl ;

警告

参考之前的警告。

6.3. 一对一( One-to-One )关系

一对一 关系目前是不被支持的,但是 可以通过 多对一 关系来模拟,因为它们 的数据库模式实际上是相同的。

7. 自定义映射

默认情况下, Wt::Dbo 会向每个被映射的表中添加这两样东西: 一个自动增加的代理主 键( id )和一个版本字段 version )。

当然,对于 新项目来说 ,这些默认行为狠有意义。但是 呢, 妳仍然可以 对映射情况进行微调,以适应任何已有 的数据库模式。

7.1. 改变或禁用默认的代理主键"id"字段

要想改变 一个被映射的类 的代理主键的字段名,或者 想禁用某个 类的 代理 主键而使用 一个自然主键的话, 妳需要特化 Wt::Dbo::dbo_traits<C> 模板。

例如,以下的代码,将Post 类的 主键字段 id 改成了 post_id

修改"id"字段(tutorial3.C)

#include <Wt/Dbo/Dbo>

namespace dbo = Wt :: Dbo ;

class Post {

public :

...

} ;

namespace Wt {

namespace Dbo {

template <>

struct dbo_traits < Post > : public dbo_default_traits {

static const char * surrogateIdField () {

return "post_id" ;

}

} ;

}

}

7.2. 修改或禁用"version"字段

要想改变乐观 并行控制版本字段 version )的字段名,或者干脆禁用某个 类的乐观 锁并行控制功能的话, 妳需要特化 Wt::Dbo::dbo_traits<C> 模板。

例如,以下的代码禁用了Post 类的乐观 锁并行控制功能:

禁用"version"字段(tutorial4.C)

#include <Wt/Dbo/Dbo>

namespace dbo = Wt :: Dbo ;

class Post {

public :

...

} ;

namespace Wt {

namespace Dbo {

template <>

struct dbo_traits < Post > : public dbo_default_traits {

static const char * versionField () {

return 0 ;

}

} ;

}

}

7.3. 指定一个自然主键

妳可能不想使用自动增加的代理主键,而是使用 一个不同的主键。

例如,以下的代码, 将User 表 的主键改成一个字符串 它的用户名 (username) (☯: 从下面的代码来看, 应当 是userId,用户编号 ),并且 被映射为 一个 叫做 user_name ☯: 从下面的代码来看, 应当 是user_ id varchar (20) 字段:

使用一个自然主键(tutorial5.C)

#include <Wt/Dbo/Dbo>

namespace dbo = Wt :: Dbo ;

class User {

public :

std :: string userId ;

template < class Action >

void persist ( Action & a )

{

dbo :: id ( a , userId , "user_id" , 20 );

}

} ;

namespace Wt {

namespace Dbo {

template <>

struct dbo_traits < User > : public dbo_default_traits {

typedef std :: string IdType ;

static IdType invalidId () {

return std :: string ();

}

static const char * surrogateIdField () { return 0 ; }

} ;

}

}

id()函数的语法与 field() 函数一致。

这里用的自然主键也可以是 :一个复合(composite)键、一个外键或一个组合(combination)。

7.4. 指定一个复合自然主键

要想将一个复合类型用作自然主键的话, 也就是说,用那种 由多个字段组成的类型作为主键, 妳需要写出 一个 对应 的C++类型。

对这个类型有一庹基本要求:默认构造函数 啊、比较操作 符(== <)啊、流(streaming)操作符啊。

使用一个复合型自然主键(tutorial6.C)

struct Coordinate {

int x , y ;

Coordinate ()

: x (- 1 ), y (- 1 ) { }

Coordinate ( int an_x , int an_y )

: x ( an_x ), y ( an_y ) { }

bool operator == ( const Coordinate & other ) const {

return x == other . x && y == other . y ;

}

bool operator < ( const Coordinate & other ) const {

if ( x < other . x )

return true ;

else if ( x == other . x )

return y < other . y ;

else

return false ;

}

} ;

std :: ostream & operator << ( std :: ostream & o , const Coordinate & c )

{

return o << "(" << c . x << ", " << c . y << ")" ;

}

然后,妳必须说明如何对这个类型进行持久化: 为它重载Dbo 的 field() 函数。

(tutorial6.C继续)

namespace Wt {

namespace Dbo {

template < class Action >

void field ( Action & action , Coordinate & coordinate , const std :: string & name ,

int size = - 1 )

{

field ( action , coordinate . x , name + "_x" );

field ( action , coordinate . y , name + "_y" );

}

}

}

以上东西做了之后,我们就可以使用 Coordinate 类型来作为自然主键类型了:

(tutorial6.C继续)

class GeoTag ;

namespace Wt {

namespace Dbo {

template <>

struct dbo_traits < GeoTag > : public dbo_default_traits

{

typedef Coordinate IdType ;

static IdType invalidId () { return Coordinate (); }

static const char * surrogateIdField () { return 0 ; }

} ;

}

}

class GeoTag {

public :

Coordinate position ;

std :: string name ;

template < class Action >

void persist ( Action & a )

{

dbo :: id ( a , position , "position" );

dbo :: field ( a , name , "name" );

}

} ;

注意,复合 型主键 还可以包含外键的, 只需要 在复合类型中储存ptr<>对象 ,并且使用 belongsTo() 声明 来将它们映射好就可以。参考 tutorial8.C ,那是一个完整的示例。

7.5. 指定外键约束

belongsTo()函数是重载的,所以 妳可以将那些 由数据库强制 要求的外键约束添加进去,例如:

  • •. NotNull: 不能为空(null)

  • •. OnUpdateCascade: 把对于 (自然)主键的变更级联地传递(cascade)到引用 它的外键上

  • •. OnUpdateSetNull: 当更新一个(自然)主键时,将引用它的那些外键设置成空

  • •. OnDeleteCascade: 删除一个对象时,级联 地删除(cascade)那些 以外键引用到此对象的对象

  • •. OnDeleteSetNull: 删除一个对象时,将那些引用 此对象的外键设置成空

下一章中, 我们将看到,如何 为那些同时(double as)也是主键的外键指定外键约束

7.6. 指定一个同时也是外键的自然主键

让我们来定义一个UserInfo 类,它提供关于用户 (User)的附加信息。 对于每个用户 (User) ,我们只允许有一个用户信息(UserInfo)对象,因此 把对用户 (User)的引用就作为用户信息 (UserInfo)的主

使用外键作为主键(tutorial7.C)

#include <Wt/Dbo/Dbo>

#include <Wt/Dbo/backend/Sqlite3>

namespace dbo = Wt :: Dbo ;

class UserInfo ;

class User ;

namespace Wt {

namespace Dbo {

template <>

struct dbo_traits < UserInfo > : public dbo_default_traits {

typedef ptr<User> IdType ;

static IdType invalidId () {

return ptr < User >();

}

static const char * surrogateIdField () { return 0 ; }

} ;

}

}

class User {

public :

std :: string name ;

dbo :: collection< dbo::ptr<UserInfo> > infos ;

template < class Action >

void persist ( Action & a )

{

dbo :: field ( a , name , "name" );

// 事实 上, 这个关系 会被约束为 hasOne() ...

dbo :: hasMany ( a , infos , dbo :: ManyToOne , "user" );

}

} ;

class UserInfo {

public :

dbo :: ptr<User> user ;

std :: string info ;

template < class Action >

void persist ( Action & a )

{

dbo :: id ( a , user , "user" , dbo :: OnDeleteCascade );

dbo :: field ( a , info , "info" );

}

} ;

void run ()

{

/*

* 设置 一个会话, 一般情况下, 只需要在程序启动时做一次

*/

dbo :: backend :: Sqlite3 sqlite3 ( ":memory:" );

sqlite3 . setProperty ( "show-queries" , "true" );

dbo :: Session session ;

session . setConnection ( sqlite3 );

session . mapClass < User >( "user" );

session . mapClass < UserInfo >( "user_info" );

/*

* 尝试创建数据库模式 (如果已 经存在,则会失败 )。

*/

session . createTables ();

dbo :: Transaction transaction ( session );

{

User * user = new User ();

user -> name = "Joe" ;

dbo :: ptr<User> userPtr = session . add ( user );

UserInfo * userInfo = new UserInfo ();

userInfo -> user = userPtr ;

userInfo -> info = "great guy" ;

session . add ( userInfo );

transaction . commit ();

}

{

dbo :: Transaction transaction ( session );

dbo :: ptr<UserInfo> info = session . find < UserInfo >();

std :: cerr << info -> user -> name << " is a " << info -> info << std :: endl ;

transaction . commit ();

}

}

int main ( int argc , char ** argv )

{

run ();

}

如妳所见, 这个示例中, 我们实际上需要的是一对一的关系,但是Dbo 中目前不支持一对一的关系,于是 我们就使用多对一的关系来模拟了(实际 上这两种关系在结构化查询语言中的表示方式是相同的 )。

运行的时候,会输出这些东西:

begin transaction

create table "user" (

"id" integer primary key autoincrement ,

"version" integer not null ,

"name" text not null

)

create table "user_info" (

"version" integer not null ,

"user_id" bigint ,

"info" text not null ,

primary key ( "user_id" ),

constraint "fk_user_info_user" foreign key ( "user_id" ) references "user" ( "id" ) on delete cascade

)

commit transaction

begin transaction

insert into "user" ( "version" , "name" ) values (?, ?)

insert into "user_info" ( "version" , "user_id" , "info" ) values (?, ?, ?)

commit transaction

begin transaction

select version , "user_id" , "info" from "user_info"

select "version" , "name" from "user" where "id" = ?

Joe is a great guy

commit transaction

8. 事务和并发

从数据库中读取数据,或者向数据库中刷新变更,都需要具有 一个活跃的事务。事务 Transaction )是一个资源初始化即占用(RIIA (Resource-Initialization-is-Acquisition))类, 它会同时保证以下两点:并发会话之间 的隔离; 向数据库提交变更的原子性。

这个库实现了乐观锁, 这就使得可以检测( 而不是避免 )并发 的修改。 这是一种广受建议并且广泛使用的策略,用来 在一种 可扩展的尺度 上处理并发问题, 而不 用向数据库中加入 写锁(write locks)。 为了检测到并 修改, 我们向每个表中加入了一个 version 字段, 这个字段 的值 在每次修改时都会增 在进行修改(例如更新 或删除某个对象 )时, 会检查数据库 中该记录的版本 ,确认 一下它是不是 与最初从数据库里读取到的对象的版本一致。

注意

事务隔离级别

这个库的 乐观 策略 所要求的最小隔离级别是 提交 读(Read Committed) 在一个事务中进行的变更,只有在它提交过之后才立即变得 对其它会话可 这通常是一个数据库所支持的最低隔离级别。

Transaction 是一个轻量级的代理, 它引用了一个 逻辑 事务:多个 (通常 是嵌套的 )事务(Transaction)对象 同时 (simultaneously)被 实例化 在这些事务都被提交之后,逻辑事务才会 被提交。通过 这种方式, 妳可以轻易地保护那些需要 这种事务对象 的数据库访问能力的单 个函数, 事务会在 找得到更 大规模 (wider)的事务的情况下自动加入其中。事实 上, 事务对象 会延迟 到必要 的时候才 打开一个真正的事务,所以 妳大可以 为了确保某 段代码 是原子的而实例 化一个事务对象, 这不会有什么性能损失 ,即使 妳并不确定 是否会真正操作数据库也没关系。

事务是有可能失败的, 对失败的事务进行处理是一个群策群力( ☯: 本座认为 意思 是指 库和程序猿都要出力,原文是integral aspect )的过程。 当库检测到一次并发修改时, 会抛出 StaleObjectException 异常。 还可能会抛出其它的一些异常,包括后端驱动 的一些异常,例如 :数据库模式 与当前映射 不兼容。 还有可能,业务逻辑检测 到了某些问题,然后抛出异常,导致事务 被回滚。 当一个事务被回滚之后, 被修改过的数据库对象 就没有 被成功地 同步到数据库中,但是 可以在稍后 的新事务中 同步。

当然了,狠多异常都是致命的。然而 ,有个异常是值得格外关注的: StaleObjectException 可采用不同的策略来处置这个异常。无论采用什么处理方法 妳最少应当重新读取 reread() )一下 已经过期 (stale)的数据库对象,然后才 在新的事务中提交变更

9. 安装Wt::Dbo

Wt::Dbo 是包含在Wt 中的,所以 可作为这个库的一部分安装 。并且 妳的操作系统可能已经 有这样一个标准软件包了。

然而,这个库根本 不依赖Wt,因而 可以单独编译 、安装及使用。利用 一个Wt 源代码包(并且 在一个类UNIX 的环境中 ), 妳可以按以下步骤来仅仅编译安装 Wt::Dbo

从源代码安装 Wt::Dbo UNIX系统)

$ cd wt-xxx

$ mkdir build

$ cd build

$ cmake ../ # 可能需要其它 的选项,用来寻找boost、postgres,设置安装目录,……

$ cd src/Wt/Dbo

$ make

$ sudo make install

参考Wt安装向导

Olivia Alaina May

Olivia Alaina May

动了外科手术的鳄鱼

烧过的公交车

海怪

唐莉

贝克汉

Your opinions
Your name:Email:Website url:Opinion content:
- no title specified

HxLauncher: Launch Android applications by voice commands

 
Recent comments
2017年4月~2019年4月垃圾短信排行榜Posted at:Thu Sep 26 04:51:48 2024
Qt5.7文档翻译:QWebEngineCookieStore类,QWebEngineCookieStore ClassPosted at:Fri Aug 11 06:50:35 2023盲盒kill -9 18289 Grebe.20230517.211749.552.mp4